개요
프론트에서 권한에 따른 페이지 접근을 제어하기 위한 라우팅을 어떻게 구현하고 관리할 것인지 고민
시도 1. (middleware)
Next.js에서 제공하는 middleware를 사용해서 권한에 따른 리다이렉션
상세 구현 내용
middleware.ts
1// check하고 하는 request url
2export const config = {
3 matcher: ["/", "/control/:path*", "/enablement/:path*", "/login/:path*", "/manage/:path*"],
4};
5
6export default async function middleware(request: NextRequest) {
7 ...
8
9 // 1. 쿠키에 session_id가 없으면 바로 로그인 페이지로 리다이렉트
10 if (!cookie || !cookie.includes("session_id")) {
11 if (request.nextUrl.pathname.startsWith("/login")) {
12 return NextResponse.next();
13 }
14 return NextResponse.redirect(new URL("/login", request.url));
15 }
16
17 try {
18 const { code, result } = await getActivation(cookie || "");
19 const isLogin = ![401001].includes(code);
20 const isActivated = result["is_activated"];
21
22 // 2. login 페이지로 접근했을 때
23 if (request.nextUrl.pathname.startsWith("/login")) {
24 if (isLogin && isActivated) {
25 return NextResponse.redirect(new URL("/", request.url));
26 }
27
28 if (isLogin && !isActivated) {
29 const url = getRedirectURL("/enablement/license", request.url, "/");
30 return NextResponse.redirect(url);
31 }
32
33 if (!isLogin && isActivated) {
34 return NextResponse.next();
35 }
36
37 if (!isLogin && !isActivated) {
38 return NextResponse.next();
39 }
40 }
41
42 // 3. enablement 페이지로 접근했을 때
43 if (request.nextUrl.pathname.startsWith("/enablement")) {
44 if (isLogin && isActivated) {
45 return NextResponse.redirect(new URL("/", request.url));
46 }
47
48 if (isLogin && !isActivated) {
49 return NextResponse.next();
50 }
51
52 if (!isLogin && isActivated) {
53 const url = getRedirectURL("/login", request.url, "/");
54 return NextResponse.redirect(url);
55 }
56
57 if (!isLogin && !isActivated) {
58 const url = getRedirectURL("/login", request.url, "/enablement/license");
59 return NextResponse.redirect(url);
60 }
61 }
62
63 // 4. 그 외 페이지로 접근했을 때
64 if (isLogin && isActivated) {
65 return NextResponse.next();
66 }
67
68 if (isLogin && !isActivated) {
69 const url = getRedirectURL("/enablement/license", request.url, "/");
70 return NextResponse.redirect(url);
71 }
72
73 if (!isLogin && isActivated) {
74 const url = getRedirectURL("/login", request.url, "/");
75 return NextResponse.redirect(url);
76 }
77
78 if (!isLogin && !isActivated) {
79 const url = getRedirectURL("/login", request.url, "/enablement/license");
80 return NextResponse.redirect(url);
81 }
82 } catch (e) {
83 // 5. 에러가 발생한 경우 에러 페이지로 리다이렉트
84 const error = e as CustomError;
85
86 if (error.data) {
87 return NextResponse.redirect(
88 new URL(
89 `/_error?errorCode=${error.data.code}&errorMessage=${error.data.message}&redirectURL=${request.nextUrl.pathname}`,
90 request.url
91 )
92 );
93 }
94
95 return NextResponse.redirect(new URL(`/_error?errorCode=500&redirectURL=${request.nextUrl.pathname}`, request.url));
96 }
문제 사항
- URL 캐싱 때문에 페이지가 변경될 때마다 실행되지 않는다.
시도 2. (server side)
getServerSideProps
를 사용해서 매 페이지 실행마다 서버에서 리다이렉트
상세 구현 내용
auth-router.ts
1import { type GetServerSidePropsContext, type Redirect } from "next";
2
3import { getAuthConditions } from "./get-auth-conditions";
4
5type GetRoute = (request: GetServerSidePropsContext["req"]) => Promise<Response>;
6
7type Response =
8 | { redirect?: Redirect; redirectURL?: string; props: Record<string, never> }
9 | { props: Record<string, never> };
10
11export const authRouter: GetRoute = async (request) => {
12 const { isLoggedIn, isActivated, error } = await getAuthConditions();
13
14 if (error) {
15 return response.redirect(`/_error?errorCode=${error.code}&errorMessage=${error.message}`);
16 }
17
18 // 이하 middleware와 조건부 라우팅 로직과 같음
19 // ...
20
21const response = {
22 next: () => ({
23 props: {},
24 }),
25
26 redirect: (url: string) => ({
27 redirect: {
28 destination: url,
29 permanent: false,
30 },
31 props: {},
32 }),
33};
page.tsx
1const Page = () => {
2 return (
3 <></>
4 );
5};
6
7export const getServerSideProps: GetServerSideProps = async ({ req }) => {
8 return await authRouter(req);
9};
10
11export default Page;
문제 사항
- 모든 페이지마다
getServerSideProps
를 작성해줘야 하는 번거로움이 있다.
- 페이지를 이동할 때마다 서버로 요청이 간다.
다른 방법. (HOC)
페이지 컴포넌트를 감싸는 고차 컴포넌트를 사용해서 이동마다 리다이렉트를 실행
(카카오 페이 증권에서는 해당 방식 사용)
에러 상황에 따라 모달 등 UI 대처의 폭이 넓어진다.
문제 사항
-
페이지가 로드되고 리다이렉트가 발생하므로 잠시 해당 페이지가 보여서 UX 적으로 좋지 않다.
(flickering issue?)
문제 개선 방법
- HOC에서 로직을 수행하는 지연 시간 동안 로딩 UI를 보여준다.
결론
Server Side에서 리다이렉트 시키는 것으로 결정
이유
- 깜빡이는 문제 또는 페이지 이동간 로딩 UI를 넣는 것은 UX상 좋지 않음
- 페이지 이동마다 서버로 요청이 가는 것은 현재 프로덕트에서 성능상 크게 문제되지 않음